<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Trace Viewer V2</title>
    <link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css">
    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
    <script src="https://unpkg.com/element-plus"></script>
    <script src="https://unpkg.com/@element-plus/icons-vue"></script>
    <script src="https://d3js.org/d3.v7.min.js"></script>
    <style>
        .trace-container {
            display: flex;
            flex-direction: column;
            height: 100vh;
            font-family: 'Helvetica Neue', Arial, sans-serif;
        }

        .trace-content {
            display: flex;
            flex: 1;
            overflow: hidden;
        }

        .trace-list {
            width: 30%;
            overflow-y: auto;
            border-right: 1px solid #e6e6e6;
        }

        .trace-detail {
            width: 70%;
            padding: 20px;
            overflow-y: auto;
        }

        .timeline {
            height: 120px;
            min-width: 100%;
            background: #f5f5f5;
            padding: 10px;
            border-bottom: 1px solid #e6e6e6;
        }

        .span-node {
            cursor: pointer;
            padding: 5px 0;
        }

        .span-node:hover {
            background-color: #f0f7ff;
        }

        .span-duration {
            color: #666;
            font-size: 12px;
        }

        .timeline-bg {
            fill: #f8f8f8;
        }

        .axis--x path {
            stroke: #333;
            stroke-width: 1px;
        }

        .axis--x line {
            stroke: #ddd;
        }

        .axis--x text {
            font-size: 12px;
            fill: #333;
        }

        .timeline-visualization {
            flex: 1;
            padding: 20px;
            background: #f8f8f8;
            border-left: 1px solid #e6e6e6;
            overflow-y: auto;
            position: relative;
            height: 100%;
        }

        .span-visualization-container {
            position: relative;
            height: 100%;
            margin-top: 40px;
        }

        .span-visualization {
            height: 20px;
            background: #409EFF;
            position: absolute;
            margin-top: 2px;
            border-radius: 2px;
        }

        .span-label {
            font-size: 8px;
            color: white;
            padding: 0 5px;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }

        .trace-timeline {
            background: #f5f5f5;
            padding: 10px;
            border-radius: 4px;
        }

        .trace-timeline svg {
            display: block;
        }

        .trace-timeline .axis path {
            stroke: #333;
            stroke-width: 1px;
        }

        .trace-timeline .axis line {
            stroke: #ddd;
        }

        .trace-timeline .axis text {
            font-size: 12px;
            fill: #333;
        }
    </style>
</head>

<body>
    <div id="app" class="trace-container">
        <!-- Top timeline -->
        <div class="timeline">
            <div id="timeline-chart"></div>
        </div>
        <div class="trace-content">
            <div class="trace-list">
                <div style="padding: 10px; border-bottom: 1px solid #e6e6e6;">
                    <el-input v-model="searchTraceId" placeholder="输入Trace ID搜索" style="width: 100%;"
                        @keyup.enter="searchByTraceId">
                        <template #append>
                            <el-button @click="searchByTraceId">
                                <el-icon>
                                    <search />
                                </el-icon>
                            </el-button>
                        </template>
                    </el-input>
                </div>
                <el-tree :data="traceTree" node-key="span_id" :props="treeProps" :expand-on-click-node="false"
                    @node-click="handleNodeClick" :default-expanded-keys="expandedNodes">
                    <template #default="{ node, data }">
                        <span class="span-node">
                            {{ data.name }}
                            <span class="span-duration">({{ data.duration_ms.toFixed(2) }}ms)</span>
                        </span>
                    </template>
                </el-tree>
            </div>

            <div class="timeline-visualization" v-if="selectedSpan" v-html="renderTimelineVisualization()">
            </div>
        </div>
        <!-- Span detail -->
        <el-dialog v-model="dialogVisible" title="Span Details" width="70%">
            <el-descriptions :column="2" border>
                <el-descriptions-item label="Trace ID">{{ selectedSpan.trace_id }}</el-descriptions-item>
                <el-descriptions-item label="Span ID">{{ selectedSpan.span_id }}</el-descriptions-item>
                <el-descriptions-item label="Parent Span ID">{{ selectedSpan.parent_id || 'None'
                    }}</el-descriptions-item>
                <el-descriptions-item label="Name">{{ selectedSpan.name }}</el-descriptions-item>
                <el-descriptions-item label="Status">
                    <div style="display: flex; justify-content: space-between; align-items: center;">
                        <span :style="{color: selectedSpan.status.code === 'StatusCode.ERROR' ? '#F56C6C' : ''}">
                            {{ selectedSpan.status.code }}
                        </span>
                        <el-button v-if="selectedSpan.status.code === 'StatusCode.ERROR'" type="text" size="small"
                            @click="showStacktrace = true" icon="View" style="color: #F56C6C">
                            View Stack
                        </el-button>
                    </div>
                </el-descriptions-item>
                <el-descriptions-item label="Start Time">{{ selectedSpan.start_time}}</el-descriptions-item>
                <el-descriptions-item label="End Time">{{ selectedSpan.end_time }}</el-descriptions-item>
                <el-descriptions-item label="Duration">{{ selectedSpan.duration_ms.toFixed(2) }}
                    ms</el-descriptions-item>
            </el-descriptions>
            <el-card style="margin-top: 20px;">
                <template #header>
                    <h4>Attributes</h4>
                </template>
                <pre style="
                    max-height: 400px;
                    overflow: auto;
                    white-space: pre-wrap;
                    word-break: break-all;
                    background: #f8f8f8;
                    padding: 10px;
                    border-radius: 4px;
                ">{{ formatAttributes(selectedSpan.attributes) }}</pre>
            </el-card>
        </el-dialog>
        <el-dialog v-model="showStacktrace" title="Stacktrace Details" width="70%">
            <pre>{{ formatStacktrace(selectedSpan.attributes?.['exception.stacktrace'] || "No stacktrace available") }}</pre>
        </el-dialog>
    </div>

    <script>
        const { createApp, ref, onMounted, nextTick } = Vue;
        const { Search } = ElementPlusIconsVue;
        createApp({
            setup() {
                const traces = ref([]);
                const traceTree = ref([]);
                const selectedSpan = ref(null);
                const expandedNodes = ref([]);
                const searchTraceId = ref('');
                const showStacktrace = ref(false);

                const treeProps = {
                    label: 'name',
                    children: 'children'
                };
                const dialogVisible = ref(false);

                function searchByTraceId() {
                    if (!searchTraceId.value) {
                        buildTraceTree();
                        return;
                    }
                    const filtered = traces.value.filter(trace =>
                        trace.trace_id.includes(searchTraceId.value)
                    );

                    const tree = [];
                    filtered.forEach(trace => {
                        if (trace.root_span && trace.root_span.length > 0) {
                            const root = buildSpanTree(trace.root_span[0]);
                            tree.push(root);
                        }
                    });
                    traceTree.value = tree;
                }

                function initTimeline() {
                    const timelineContainer = document.getElementById('timeline-chart');
                    const width = timelineContainer.clientWidth;
                    const height = 100;
                    const margin = { top: 20, right: 20, bottom: 30, left: 20 };

                    const svg = d3.select(timelineContainer)
                        .append('svg')
                        .attr('width', width)
                        .attr('height', height);

                    const now = new Date();
                    const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);

                    const x = d3.scaleTime()
                        .domain([oneDayAgo, now])
                        .range([margin.left, width - margin.right]);

                    svg.append('g')
                        .attr('transform', `translate(0,${height - margin.bottom})`)
                        .call(d3.axisBottom(x)
                            .ticks(d3.timeHour.every(2))
                            .tickFormat(d3.timeFormat("%H:%M")));

                    svg.append('g')
                        .attr('class', 'grid')
                        .attr('transform', `translate(0,${height - margin.bottom})`)
                        .call(d3.axisBottom(x)
                            .ticks(d3.timeMinute.every(10))
                            .tickSize(-5)
                            .tickFormat(''));

                    if (traces.value && traces.value.length > 0) {
                        const colorScale = d3.scaleOrdinal()
                            .domain(traces.value.map((_, i) => i))
                            .range(d3.schemeCategory10);
                        traces.value.forEach((trace, index) => {
                            if (trace.root_span && trace.root_span.length > 0) {
                                const span = trace.root_span[0];
                                const startTime = new Date(span.start_time);
                                const endTime = new Date(span.end_time);
                                const duration = endTime - startTime;

                                if (startTime >= oneDayAgo && startTime <= now) {
                                    svg.append('rect')
                                        .attr('x', x(startTime))
                                        .attr('y', margin.top + 30)
                                        .attr('width', Math.max(3, x(endTime) - x(startTime)))
                                        .attr('height', 20)
                                        .attr('fill', colorScale(index))
                                        .attr('rx', 2)
                                        .attr('opacity', 0.7)
                                        .on('mouseover', function () {
                                            d3.select(this).attr('opacity', 1);
                                        })
                                        .on('mouseout', function () {
                                            d3.select(this).attr('opacity', 0.7);
                                        });
                                }
                            }
                        });
                    }
                }


                function renderTimelineVisualization() {
                    if (!selectedSpan.value) return '';

                    const currentTrace = traceTree.value.find(t => t.trace_id === selectedSpan.value.trace_id);
                    if (!currentTrace) return '';

                    const rootSpan = currentTrace.root_span?.[0] || currentTrace;
                    let minTime = new Date(rootSpan.start_time).getTime();
                    let maxTime = new Date(rootSpan.end_time).getTime();

                    const timelineContainer = document.createElement('div');
                    timelineContainer.className = 'trace-timeline';
                    timelineContainer.style.height = '60px';
                    timelineContainer.style.marginBottom = '20px';
                    timelineContainer.style.width = '100%';

                    const svg = d3.select(timelineContainer)
                        .append('svg')
                        .attr('width', '100%')
                        .attr('height', '100%')
                        .attr('viewBox', '0 0 1000 60');

                    const margin = { top: 10, right: 0, bottom: 30, left: 0 };
                    const width = 1000 - margin.left - margin.right;
                    const height = 60 - margin.top - margin.bottom;

                    const g = svg.append('g')
                        .attr('transform', `translate(${margin.left},${margin.top})`);


                    const x = d3.scaleTime()
                        .domain([new Date(minTime), new Date(maxTime)])
                        .range([0, width]);

                    g.append('g')
                        .attr('class', 'axis axis--x')
                        .attr('transform', `translate(0,${height})`)
                        .call(d3.axisBottom(x)
                            .ticks(5)
                            .tickFormat(d3.timeFormat("%H:%M:%S.%L")));

                    g.selectAll(".grid-line")
                        .data(x.ticks(5))
                        .enter().append("line")
                        .attr("class", "grid-line")
                        .attr("x1", d => x(d))
                        .attr("x2", d => x(d))
                        .attr("y1", 0)
                        .attr("y2", height)
                        .attr("stroke", "#eee")
                        .attr("stroke-width", 1);

                    const timelineHtml = timelineContainer.outerHTML;

                    function renderSpans(span, depth = 0, rowIndex = 0) {
                        const spanStart = new Date(span.start_time).getTime();
                        const spanEnd = new Date(span.end_time).getTime();
                        const position = Math.min(13, Math.max(5, ((spanStart - minTime) / (maxTime - minTime)) * 10 * 0.9 + 5));
                        const width = Math.min(92, Math.max(2, ((spanEnd - spanStart) / (maxTime - minTime)) * 100 * 0.9 + 2));
                        //const position = ((spanStart - minTime) / (maxTime - minTime)) * 10 * 0.9 + 5;
                        //const width = ((spanEnd - spanStart) / (maxTime - minTime)) * 100 * 0.9 + 2;

                        const minWidth = 0.5;
                        const adjustedWidth = Math.max(width, minWidth);

                        const row = rowIndex * 24;

                        let childrenHtml = '';
                        let nextRowIndex = rowIndex + 1;

                        if (span.children && span.children.length > 0) {
                            childrenHtml = span.children.map(child => {
                                const childHtml = renderSpans(child, depth + 1, nextRowIndex);
                                nextRowIndex += countSpans(child);
                                return childHtml;
                            }).join('');
                        }
                        return `
                            <div class="span-visualization" 
                                style="top: ${row}px; 
                                        left: ${position}%; 
                                        width: ${adjustedWidth}%;
                                        background: ${span.status.code === 'StatusCode.ERROR' ? '#F56C6C' : '#409EFF'};
                                        opacity: ${span.span_id === selectedSpan.value.span_id ? 1 : 0.6}"
                                        onclick="window.handleSpanClick.call(this, ${JSON.stringify(span).replace(/"/g, '&quot;')})">
                                <span class="span-label">${span.duration_ms} ${span.name}</span>
                            </div>
                            ${childrenHtml}
                        `;
                    }

                    return `
                        <h3>Timeline Visualization</h3>
                        ${timelineHtml}
                        <div class="span-visualization-container" style="height: ${traceTree.value.length * 24 + 100}px">
                            ${renderSpans(rootSpan)}
                        </div>
                    `;
                }

                function countSpans(span) {
                    let count = 1;
                    if (span.children && span.children.length > 0) {
                        span.children.forEach(child => {
                            count += countSpans(child);
                        });
                    }
                    return count;
                }

                async function fetchTraces() {
                    try {
                        const response = await fetch('/api/trace/list');
                        const data = await response.json();
                        traces.value = data.data;
                        buildTraceTree();
                        initTimeline();
                    } catch (error) {
                        console.error('Error loading traces:', error);
                    }
                }

                function buildTraceTree() {
                    const tree = [];
                    traces.value.forEach(trace => {
                        if (trace.root_span && trace.root_span.length > 0) {
                            const root = buildSpanTree(trace.root_span[0]);
                            tree.push(root);
                        }
                    });
                    traceTree.value = tree;
                }

                function buildSpanTree(span) {
                    const node = {
                        ...span,
                        children: []
                    };

                    if (span.children && span.children.length > 0) {
                        span.children.forEach(child => {
                            node.children.push(buildSpanTree(child));
                        });
                    }

                    return node;
                }

                function handleNodeClick(data) {
                    selectedSpan.value = data;
                    nextTick(() => {
                        renderTimelineVisualization();
                    });
                }

                function handleSpanClick(data) {
                    selectedSpan.value = data;
                    dialogVisible.value = true;
                    if (!expandedNodes.value.includes(data.span_id)) {
                        expandedNodes.value.push(data.span_id);
                    }
                }

                function formatTime(timestamp) {
                    return timestamp.split('.')[0];
                }

                function formatAttributes(attrs) {
                    return JSON.stringify(attrs, null, 2);
                }

                function formatStacktrace(stacktrace) {
                    if (!stacktrace) return 'No stacktrace available';
                    try {
                        return JSON.stringify(JSON.parse(stacktrace), null, 2);
                    } catch {
                        return stacktrace;
                    }
                }

                onMounted(() => {
                    fetchTraces();
                    //setInterval(fetchTraces, 5000);
                    window.handleSpanClick = handleSpanClick;
                });

                return {
                    traces,
                    traceTree,
                    selectedSpan,
                    expandedNodes,
                    treeProps,
                    handleNodeClick,
                    handleSpanClick,
                    formatTime,
                    formatAttributes,
                    dialogVisible,
                    renderTimelineVisualization,
                    searchTraceId,
                    searchByTraceId,
                    showStacktrace,
                    formatStacktrace
                };
            }
        }).use(ElementPlus).component('search', Search).mount('#app');
    </script>
</body>

</html>